﻿using System;
using System.Collections.Generic;
using GammaJul.ReSharper.GammaXaml.Psi;
using GammaJul.ReSharper.GammaXaml.Psi.Xaml;
using GammaJul.ReSharper.GammaXaml.Psi.Xml;
using JetBrains.Annotations;
using JetBrains.Application.Progress;
using JetBrains.ProjectModel;
using JetBrains.ReSharper.Feature.Services.Bulbs;
using JetBrains.ReSharper.Feature.Services.Xaml.Bulbs;
using JetBrains.ReSharper.Intentions.Extensibility;
using JetBrains.ReSharper.Psi.ExtensionsAPI.Tree;
using JetBrains.ReSharper.Psi.Parsing;
using JetBrains.ReSharper.Psi.Tree;
using JetBrains.ReSharper.Psi.Util;
using JetBrains.ReSharper.Psi.Xaml;
using JetBrains.ReSharper.Psi.Xaml.Impl;
using JetBrains.ReSharper.Psi.Xaml.Impl.Util;
using JetBrains.ReSharper.Psi.Xaml.Tree;
using JetBrains.ReSharper.Psi.Xaml.Tree.MarkupExtensions;
using JetBrains.ReSharper.Psi.Xml.Tree;
using JetBrains.TextControl;
using JetBrains.Util;

namespace GammaJul.ReSharper.GammaXaml.ContextActions.Xaml {

	/// <summary>
	/// Converts a <c>{Binding RelativeSource={x:Static RelativeSource.TemplatedParent}}</c> to a <c>{TemplateBinding}</c>.
	/// </summary>
	[ContextAction(Name = "Convert to TemplateBinding", Group = "XAML", Description = "Converts a Binding with RelativeSource TemplatedParent to a TemplateBinding.")]
	public class BindingToTemplateBindingAction : ContextActionBase {
		private readonly XamlContextActionDataProvider _provider;

		public override string Text {
			get { return "Convert 'Binding' to 'TemplateBinding'"; }
		}

		protected override Action<ITextControl> ExecutePsiTransaction(ISolution solution, IProgressIndicator progress) {
			var binding = _provider.FindNodeAtCaret<IBinding>();

			var markup = binding as IBindingMarkup;
			if (markup != null)
				ReplaceBindingMarkup(markup);

			else {
				var element = binding as IBindingElement;
				if (element != null)
					ReplaceBindingTag(element);
			}

			return null;
		}

		private void ReplaceBindingMarkup([NotNull] IMarkup markup) {
			XamlElementFactory elementFactory = XamlElementFactory.GetElementFactory(markup);
			XmlTokenTypes xmlTokenTypes = XmlTokenTypes.GetInstance<XamlLanguage>();

			// replace "Binding" name node with "TemplateBinding"
			IXamlIdentifier nameNode = markup.NameNode;
			if (nameNode != null)
				nameNode.ReplaceBy(elementFactory.CreateIdentifier("TemplateBinding", xmlTokenTypes));

			var attributeList = (IAttributeListMarkupValue) markup.Value;
			Assertion.Assert(attributeList != null, "attributeList != null");
			IMarkupAttribute relSourceAttr = markup.GetAttribute("RelativeSource");
			Assertion.Assert(relSourceAttr != null, "relSourceAttr != null");
			var attributes = attributeList.Attributes;

			// there is only a RelativeSource attribute that will be deleted: remove the entire attribute list and we're done
			if (attributes.Count == 1)
				attributeList.Delete();

			else {
				
				// deletes the RelativeSource attribute with its whitespaces and comma
				ModificationUtil.DeleteChildRange(GetNodesToDelete(relSourceAttr, attributes[0] != relSourceAttr));

				// replace Path attribute with Property
				IMarkupAttribute pathAttribute = markup.GetAttribute("Path");
				if (pathAttribute != null) {
					IXamlIdentifier pathNameNode = pathAttribute.NameNode;
					if (pathNameNode != null)
						pathNameNode.ReplaceBy(elementFactory.CreateIdentifier("Property", xmlTokenTypes));
				}

			}

			_provider.PsiServices.MarkAsDirty(_provider.SourceFile);
		}

		/// <summary>
		/// Gets the nodes to delete for a given markup attribute that will be deleted.
		/// This includes whitespaces after (or before if the search is backwards) the attribute and commas.
		/// </summary>
		/// <param name="node">The markup attribute.</param>
		/// <param name="backwards">Whether to search backwards.</param>
		/// <returns>A <see cref="TreeRange"/> representing the nodes to delete/</returns>
		[NotNull]
		private static TreeRange GetNodesToDelete([NotNull] ITreeNode node, bool backwards) {
			ITreeNode endNode = node;
			bool lastWasComma = false;
			foreach (ITreeNode rightSibling in backwards ? node.LeftSiblings() : node.RightSiblings()) {
				var token = rightSibling as ITokenNode;
				if (token == null)
					break;

				TokenNodeType tokenType = token.GetTokenType();
				if (tokenType.IsWhitespace) {
					endNode = rightSibling;
					if (lastWasComma)
						break;
				}
				else {
					var xamlToken = token as IXamlToken;
					if (xamlToken != null && tokenType == xamlToken.XamlTokenTypes.MARKUP_COMMA) {
						endNode = rightSibling;
						lastWasComma = true;
					}
					else
						break;
				}
			}
			return backwards ? new TreeRange(endNode, node) : new TreeRange(node, endNode);
		}

		private void ReplaceBindingTag([NotNull] IXmlTag tag) {
			XamlElementFactory elementFactory = XamlElementFactory.GetElementFactory(tag);
			XmlTokenTypes xmlTokenTypes = XmlTokenTypes.GetInstance<XamlLanguage>();

			// replace "Binding" name node with "TemplateBinding"
			tag.GetNameNode().ReplaceBy(elementFactory.CreateIdentifier("TemplateBinding", xmlTokenTypes));

			// replace Path attribute with Property
			IXmlAttribute pathAttribute = tag.GetAttribute("Path");
			if (pathAttribute != null)
				pathAttribute.NameNode.ReplaceBy(elementFactory.CreateIdentifier("Property", xmlTokenTypes));

			// deletes the RelativeSource attribute
			IXmlAttribute relSourceAttr = tag.GetAttribute("RelativeSource");
			if (relSourceAttr != null)
				tag.RemoveAttribute(relSourceAttr);

			_provider.PsiServices.MarkAsDirty(_provider.SourceFile);
		}

		private static RelativeSourceMode GetRelativeSourceMode([CanBeNull] ITreeNode value) {
			var relSourceUsage = value as IRelativeSourceUsage;
			if (relSourceUsage != null)
				return relSourceUsage.Mode;

			var xStatic = value as IXStatic;
			if (xStatic != null)
				return xStatic.GetRelativeSourceMode();

			var propAttrValue = value as IPropertyAttributeValue;
			if (propAttrValue != null) {
				foreach (IMarkup markup in propAttrValue.GetMarkups()) {
					RelativeSourceMode mode = GetRelativeSourceMode(markup);
					if (mode != RelativeSourceMode.UNKNOWN)
						return mode;
				}
			}

			return RelativeSourceMode.UNKNOWN;
		}

		/// <summary>
		/// Returns whether some binding attributes can be converted to template binding attributes.
		/// </summary>
		/// <typeparam name="TAttribute">The attribute types.</typeparam>
		/// <param name="attributes">The attributes.</param>
		/// <param name="nameGetter">A function returning a name for an attribute.</param>
		/// <param name="valueGetter">A function returning a value for an attribute.</param>
		/// <returns><c>true</c> if all <paramref name="attributes"/> can be converted to template binding attributes, <c>false</c> otherwise.</returns>
		private bool CanConvertToTemplateBinding<TAttribute>([NotNull] IEnumerable<TAttribute> attributes, [NotNull] Func<TAttribute, string> nameGetter, [NotNull] Func<TAttribute, ITreeNode> valueGetter) {
			Silverlightness? silverlightness = null;
			bool isTemplatedParentRelSource = false;

			foreach (TAttribute attr in attributes) {
				switch (nameGetter(attr)) {

					case "Path":
						break;

					// we must have a RelativeSource TemplatedParent
					case "RelativeSource":
						if (GetRelativeSourceMode(valueGetter(attr)) == RelativeSourceMode.TEMPLATED_PARENT)
							isTemplatedParentRelSource = true;
						else
							return false;
						break;

					// Converter and ConverterParameter aren't available in Silverlight TemplateBinding but are in WPF
					case "Converter":
					case "ConverterParameter":
						if (silverlightness == null)
							silverlightness = SilverlightUtil.GetSilverlightness(_provider.PsiModule);
						if (silverlightness.Value != Silverlightness.NO_SILVERLIGHT)
							return false;
						break;

					// other binding properties aren't supported in TemplateBinding
					default:
						return false;
				}
			}
			return isTemplatedParentRelSource;
		}

		public override bool IsAvailable(IUserDataHolder cache) {
			var binding = _provider.FindNodeAtCaret<IBinding>();
			if (binding == null)
				return false;

			// {Binding}
			var markup = binding as IBindingMarkup;
			if (markup != null)
				return CanConvertToTemplateBinding(markup.GetAttributes(), attr => attr.Name, attr => attr.Value);

			// <Binding>
			var element = binding as IBindingElement;
			if (element != null)
				return CanConvertToTemplateBinding(element.GetAttributes(), attr => attr.AttributeName, attr => attr.Value);

			return false;
		}

		public BindingToTemplateBindingAction([NotNull] XamlContextActionDataProvider provider) {
			if (provider == null)
				throw new ArgumentNullException("provider");
			_provider = provider;
		}
	}

}